/// File: Robotics4.NET\BodyMap\Roblet.cs
/// 
/// ------------------------------------------------------------
/// Copyright (c) 2004
///   Antonio Cisternino (cisterni.unipi.it),
///   Diego Colombo      (colombod.unipi.it),
/// 
/// The use and distribution terms for this software are 
/// contained in the file named license.txt, which can be found 
/// in the root of this distribution.
/// By using this software in any fashion, you are agreeing to 
/// be bound by the terms of this license.
///
/// You must not remove this notice, or any other, from this
/// software.
/// ------------------------------------------------------------

using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Reflection;
#if WIN
using System.Collections.Generic;
using R4NThread = System.Threading.Thread;
using R4NRobletMessagesList = System.Collections.Generic.List<Robotics4.NET.RobletMessage>;
using R4NFriendsList = System.Collections.Generic.Dictionary<string, Robotics4.NET.FriendInfo>;
using R4NTimerEntryList = System.Collections.Generic.List<Robotics4.NET.RobletBase.TimerEntry>;
#elif CF
using OpenNETCF.Threading;
using R4NThread = OpenNETCF.Threading.ThreadEx;
#endif
using System.Xml;
using System.Xml.Serialization;
using System.IO;

namespace Robotics4.NET
{
    public delegate void OnSignalHandler(RobletMessage m);

    /// <summary>
    /// Attribute class to describe input messages accepted by a Roblet. This
    /// is part of the declarative architecture of Roblet definition: each Roblet
    /// defines the messages as classes inheriting from RobletMessage and uses
    /// this attribute to declare that the message is an input message.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class InputMessageAttribute : Attribute
    {
        public Type Type;
        public InputMessageAttribute(Type t)
        {
            Type = t;
        }
    }

    /// <summary>
    /// Attribute class to describe output messages accepted by an Roblet. This
    /// is part of the declarative architecture of Roblet definition: each Roblet
    /// defines the messages as classes inheriting from RobletMessage and uses
    /// this attribute to declare that the message is an output message.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class OutputMessageAttribute : Attribute
    {
        public Type Type;
        public OutputMessageAttribute(Type t)
        {
            Type = t;
        }
    }

    /// <summary>
    /// This attribute is used to specify friend Roblets. A friend Roblet is an
    /// Roblet that can be target of messages from the annotated Roblet. This
    /// mechanism is required to implement full reactive behaviors.
    /// </summary>
    [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true)]
    public class FriendAttribute : Attribute
    {
        public string FriendName;
        public FriendAttribute(string friendName)
        {
            FriendName = friendName;
        }
    }

    /// <summary>
    /// Base class for Roblets. All Roblets SHOULD inherits from this class
    /// (either directly or indirectly).
    /// </summary>
    [OutputMessage(typeof(HeartBeatMessage))]
    public abstract class RobletBase : MarshalByRefObject
    {
        /// <summary>
        /// Thread running the Roblet.
        /// </summary>
        private R4NThread thread;

        internal MemoryStream OutMemStream = new MemoryStream(4096);

        internal HeartBeatMessage HBMessage;
        /// <summary>
        /// Roblet name
        /// </summary>
        internal string name;
        /// <summary>
        /// Roblet ID: this ID is the index of the entry in the RobletBroker.RunningRoblets
        /// ArrayList.
        /// </summary>
        internal int ID;
        /// <summary>
        /// Inbox of received signals. This structure is filled only if the
        /// EnqueueSignals property is set to true. You are responsible for
        /// removing entries from this list.
        /// </summary>
        protected R4NRobletMessagesList Signals;
        /// <summary>
        /// List of friends of this Roblet.
        /// </summary>
        protected R4NFriendsList Friends;   
        /// <summary>
        /// Number of times the method run() should be executed per second.
        /// If set to 0 (default) the method never run.
        /// The value of this field can be changed while the roblet is
        /// running.
        /// </summary>
        private double frequency;
        /// <summary>
        /// This mutex is used to ensure that only a thread running Exec starts.
        /// Otherwise when you change frequency there is a possible race
        /// condition: if you set frequency to 0 and a signal change it *before*
        /// the current thread exits you may get two thread executing!
        /// </summary>
        private Mutex threadRunning;
        /// <summary>
        /// Number of times the method run() should be executed per second.
        /// If set to 0 (default) the method never run.
        /// The value of this field can be changed while the roblet is
        /// running.
        /// </summary>
        internal protected double Frequency {
          get {
            return frequency;
          }
          set {
            if (frequency == value)
              return;
            if (frequency == 0 && value > 0) {
              threadRunning.WaitOne();
              frequency = value;
              thread = new R4NThread(new ThreadStart(this.Exec));
              thread.IsBackground = true;
              thread.Name = Name;
              thread.Start();
              return;
            }
            // freq != 0 && value = 0
            frequency = value;
          }
        }

    /// <summary>
		/// This is used only by DeliverSignal to cache deserialization context.
		/// </summary>
		private Type[] ctxt;

        /// <summary>
        /// Queue used to implement auto send of messages
        /// </summary>
        private R4NTimerEntryList TimerQueue;

		/// <summary>
        /// Return the thread associated with this Roblet.
        /// </summary>
        internal R4NThread Thread
        {
            get { return thread; }
        }

        /// <summary>
        /// Return the name of the Roblet. The name is guaranteed to be unique
        /// within the robot.
        /// </summary>
        public string Name
        {
            get { return name; }
        }

        /// <summary>
        /// If set to true the Roblet enqueue incoming messages (from bodymap
        /// and Roblet's friends) into Signals ArrayList.
        /// </summary>
        public bool EnqueueSignals
        {
            get
            {
                return Signals == null;
            }
            set
            {
                if (value)
                {
                    if (Signals == null)
                    {
                        Signals = new R4NRobletMessagesList();
                    }
                }
                else if (Signals != null)
                {
                    Signals = null;
                }
            }
        }

        /// <summary>
        /// Build an Roblet with the given name.
        /// </summary>
        /// <param name="n">Name of the Roblet</param>
        public RobletBase(string n)
        {
            HBMessage = new HeartBeatMessage();
            threadRunning = new Mutex(false);
            Signals = null;
            frequency = 0;
            Friends = new R4NFriendsList();
            TimerQueue = new R4NTimerEntryList();
            name = n;
        }

		private void FillContext(ArrayList type)
		{
			type.Add(typeof(RobletMessage));
			type.Add(typeof(RobletMessage.PacketType));
			InputMessageAttribute[] inp = (InputMessageAttribute[])GetType().GetCustomAttributes(typeof(InputMessageAttribute), true);
			Queue q = new Queue();
			Hashtable visited = new Hashtable();
			for (int i = 0; i < inp.Length; i++)
				q.Enqueue(inp[i].Type);
			while (q.Count > 0)
			{
				Type t = q.Dequeue() as Type;
				type.Add(t);
				visited[t] = new object();
				foreach (FieldInfo f in t.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance))
					if (!f.FieldType.Namespace.StartsWith("System") && !visited.ContainsKey(f.FieldType))
						q.Enqueue(f.FieldType);
			}
		}

		/// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        internal void DeliverSignal(XmlNode message)
        {
            if (message.Name == "FriendInfo")
            {
                if (RobletBroker.SerializerPool[typeof(FriendInfo), ""] == null)
                    RobletBroker.SerializerPool[typeof(FriendInfo), ""] = new XmlSerializer(typeof(FriendInfo));

                FriendInfo f = (FriendInfo)RobletBroker.SerializerPool[typeof(FriendInfo), ""].Deserialize(new XmlNodeReader(message));
                Friends[f.FriendRobletName] = f;
                return; // FIXME: Should we notify this to other Roblets?
            }
            if (Signal != null || Signals != null) {
                //deserialize xml message as a RobletMessage
                RobletMessage msg = null;

				if (ctxt == null)
				{
					ArrayList types = new ArrayList();
					FillContext(types);
					ctxt = (Type[])types.ToArray(typeof(Type));
				}
				Type t = typeof(object);

				foreach (Type tm in ctxt)
					if (message.Name == tm.Name)
					{
						t = tm;
						break;
					}

                if (RobletBroker.SerializerPool[t, message.Name] == null)
                    RobletBroker.SerializerPool[t, message.Name] = new XmlSerializer(t, ctxt);

                msg = RobletBroker.SerializerPool[t, message.Name].Deserialize(new XmlNodeReader(message)) as RobletMessage;

                if (Signals != null)
                  Signals.Add(msg);
                if (Signal != null)
                  Signal(msg);
              }
            OnSignal(message);
        }

        public event OnSignalHandler Signal;

        protected virtual void OnSignal(XmlNode message){}

        /// <summary>
        /// Return the interface of a roblet with the list of types needed to
        /// use XML serialization.
        /// </summary>
        /// <param name="interf">The interface of the roblet</param>
        /// <param name="ext">The list of types used by the interface</param>
        public void GetRobletInterface(out RobletInterface interf, out Type[] ext)
        {
            Type t = this.GetType();
            OutputMessageAttribute[] outm = (OutputMessageAttribute[])t.GetCustomAttributes(typeof(OutputMessageAttribute), true);
            InputMessageAttribute[] inm = (InputMessageAttribute[])t.GetCustomAttributes(typeof(InputMessageAttribute), true);
            FriendAttribute[] frm = (FriendAttribute[])t.GetCustomAttributes(typeof(FriendAttribute), true);
            interf = new RobletInterface();
            interf.InputMessages = new InterfaceEntry[inm.Length];
            interf.OutputMessages = new InterfaceEntry[outm.Length];
            interf.Friends = new string[frm.Length];
            string host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList[0].ToString();
            ArrayList types = new ArrayList();
            for (int i = 0; i < outm.Length; i++)
            {
                RobletMessage ag = (RobletMessage)Activator.CreateInstance(outm[i].Type);
                ag.RobletID = this.ID;
                ag.Name = this.name;
                ag.BrokerHost = host;
                ag.BrokerPort = RobletBroker.Broker.LocalPort;
                ag.Type = RobletMessage.PacketType.Data;
                if (interf.OutputMessages[i] == null)
                    interf.OutputMessages[i] = new InterfaceEntry();
                interf.OutputMessages[i].Message = ag;
                interf.OutputMessages[i].Schema = XSDGenerator.Generate(outm[i].Type);
                types.Add(outm[i].Type);
            }

            for (int i = 0; i < inm.Length; i++)
            {
                RobletMessage ag = (RobletMessage)Activator.CreateInstance(inm[i].Type);
                ag.RobletID = this.ID;
                ag.Name = this.name;
                ag.BrokerHost = host;
                ag.BrokerPort = RobletBroker.Broker.LocalPort;
                ag.Type = RobletMessage.PacketType.Data;
                if (interf.InputMessages[i] == null)
                    interf.InputMessages[i] = new InterfaceEntry();
                interf.InputMessages[i].Message = ag;
                interf.InputMessages[i].Schema = XSDGenerator.Generate(inm[i].Type);
                types.Add(inm[i].Type);
            }

            for (int i = 0; i < frm.Length; i++)
            {
                interf.Friends[i] = frm[i].FriendName;
            }
            types.Add(typeof(string));
            ext = (Type[])types.ToArray(typeof(Type));
        }

        internal void SendInterface()
        {
            RobletInterface interf;
            Type[] ext;
            GetRobletInterface(out interf, out ext);
            SendState(interf, ext);
        }

        public void Pulse()
        {
            SendState(HBMessage);
        }

        internal class TimerEntry {
            internal long nextRun;
            internal double frequency;
            internal RobletMessage msg;
            internal TimerEntry(double f, RobletMessage m, long nr) {
              frequency = f;
              msg = m;
              nextRun = nr;
            }
        }
        protected void AddPulseMessage(double freq, RobletMessage m) {
          RemovePulseMessage(m);
          TimerQueue.Add(new TimerEntry(freq, m, 0));
        }
        protected void RemovePulseMessage(RobletMessage m) {
          for (int i = 0; i < TimerQueue.Count; i++)
            if (TimerQueue[i].msg == m) {
              TimerQueue.RemoveAt(i);
              return;
            }
        }

        private void Exec() {
          int attempts = 50;
            while (frequency > 0 && attempts > 0)
            {
                try
                {
                    long nextRun = 0, now;
                    double interval;
                    while (frequency != 0)
                    {
                        now = DateTime.Now.Ticks;
                        if (now >= nextRun) {
                          nextRun = now + (long)(10000000.0 / frequency); // 10nth of us
                          this.Run();
                        }
                        long min = nextRun;
                        // Here we use linear search. We think it is reasonable, though
                        // if there are too many entries maybe a heap would be better.
                        foreach (TimerEntry t in TimerQueue) {
                          now = DateTime.Now.Ticks;
                          if (t.nextRun == 0 || t.nextRun <= now) {
                            SendState(t.msg);
                            t.nextRun = now + (long)(10000000.0 / t.frequency);
                          }
                          if (min > t.nextRun)
                            min = t.nextRun;
                        }
                        interval = (min - DateTime.Now.Ticks) / 10000;
                        if (interval > 10)
                            R4NThread.Sleep((int)interval);
                    }
                    Console.WriteLine("Exiting {0}", this.Name);
                }
                catch (Exception e)
                {
#if WIN
                    Console.WriteLine("Exiting with exception {0}: {1}\n{2}", this.Name, e.Message, e.StackTrace);
#elif CF
                    Console.WriteLine("Exiting with exception {0}: {1}\n{2}", this.Name, e.Message);
#endif
                    SendState(new RobletException(e));
                }
                R4NThread.Sleep(100);
                if (frequency > 0)
                  attempts--;
            }
            if (frequency > 0)
              SendState(new RobletDead());
            threadRunning.ReleaseMutex();
          }

        protected virtual void Run() { }

        /// <summary>
        /// Method used to initialize the roblet from the broker (calling
        /// Initialize directly from the constructor does not fit all
        /// the uses of a roblet).
        /// </summary>
        internal void Init(string name, int id, RobletBroker rb)
        {
            RobletBroker.Broker = rb;
            this.name = name;
            this.ID = id;
            Initialize();
        }

        protected abstract void Initialize();

        protected void SendState(RobletMessage msg, Type[] external)
        {
            msg.RobletID = this.ID;
            msg.Name = this.Name;
            RobletBroker.SendState(msg, this, external, OutMemStream);
        }

        protected void SendState(string friend, RobletMessage msg, Type[] external)
        {
            msg.RobletID = this.ID;
            msg.Name = this.Name;

			// FIXME: Does this make any sense?
			if (!Friends.ContainsKey(friend))
				return;

            FriendInfo f = Friends[friend];

            RobletBroker.SendState(f.FriendRobletID, f.FriendBrokerHost, f.FriendBrokerPort, msg, this, external, OutMemStream);
        }

        protected void SendState(RobletMessage msg)
        {
            msg.RobletID = this.ID;
            msg.Name = this.Name;
            RobletBroker.SendState(msg, this, null, OutMemStream);
        }
    }
}
